package com.demon.auto.handle;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.demon.auto.constant.CommonConstant;
import com.demon.auto.model.dto.ClientConfigDto;
import com.demon.auto.model.dto.SignClientDto;
import com.demon.auto.model.entity.SignRecordEntity;
import com.demon.auto.model.message.ExecSignMessage;
import com.demon.auto.model.message.SignProcessMessage;
import com.demon.auto.model.message.SignResultMessage;
import com.demon.auto.model.message.WebSocketMessage;
import com.demon.auto.service.SignClientService;
import com.demon.auto.service.SignRecordService;
import com.demon.auto.utils.EmailUtils;
import com.demon.auto.utils.LocalDateUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;

/**
 * 签到WebSocket处理
 *
 * @author huangrz
 * @version 1.0
 * @date 2025/02/13 14:09
 */
@Slf4j
@Component
public class SignWebSocketHandler extends TextWebSocketHandler {

    /**
     * 在线客户端 clientName, session
     */
    private static final Map<String, WebSocketSession> ONLINE_CLIENTS = new ConcurrentHashMap<>();
    /**
     * 签到过程 clientName-signRecordId, SignProcessMessage
     */
    private static final Map<String, List<SignProcessMessage>> SIGN_PROCESS_INFO = new ConcurrentHashMap<>();
    /**
     * 签到超时提醒线程 clientName, ScheduledFuture
     * （若x分钟后未收到客户端签到结果消息，则发送邮件通知）
     */
    private static final Map<String, ScheduledFuture<?>> SIGN_TIMEOUT_NOTIFY_LIST = new ConcurrentHashMap<>();
    /**
     * 最大连接数
     */
    @Value("${websocket.maximum-connect:10}")
    private Integer maximumConnect;

    @Autowired
    private SignClientService signClientService;

    @Autowired
    private SignRecordService signRecordService;

    @Autowired
    private EmailUtils emailUtils;

    @Autowired
    private TaskScheduler taskScheduler;

    /**
     * 连接成功时触发
     *
     * @param session 会话
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws IOException {
        // 连接数限制
        if (ONLINE_CLIENTS.size() > maximumConnect) {
            session.close(new CloseStatus(1008, "超过最大连接数..." + maximumConnect));
            return;
        }

        String clientName = getSessionClientName(session);
        removeClientWebsocket(clientName, "断开旧的连接");
        ONLINE_CLIENTS.put(clientName, session);
        if (!clientName.contains(CommonConstant.PAGE_CLIENT_NAME_CONTAINS)) {
            log.info("客户端 {} 已连接，当前在线客户端：{}", clientName, ONLINE_CLIENTS.keySet());
        }
    }

    /**
     * 连接关闭时触发
     *
     * @param session 会话
     * @param status  关闭状态
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        String clientName = getSessionClientName(session);
        WebSocketSession remove = ONLINE_CLIENTS.remove(clientName);
        if (remove != null && !clientName.contains(CommonConstant.PAGE_CLIENT_NAME_CONTAINS)) {
            log.info("客户端 {} 已断开，code：{}，reason：{}", clientName, status.getCode(), status.getReason());
        }
        if (!CollectionUtils.isEmpty(ONLINE_CLIENTS.keySet()) && !clientName.contains(CommonConstant.PAGE_CLIENT_NAME_CONTAINS)) {
            log.info("当前在线客户端：{}", ONLINE_CLIENTS.keySet());
        }

        ScheduledFuture<?> signTimeoutScheduledFuture = SIGN_TIMEOUT_NOTIFY_LIST.remove(clientName);
        if (signTimeoutScheduledFuture != null) {
            signTimeoutScheduledFuture.cancel(true);
        }

        // 清空签到过程信息
        if (!clientName.contains(CommonConstant.PAGE_CLIENT_NAME_CONTAINS)) {
            SIGN_PROCESS_INFO.keySet().stream().filter(key -> key.startsWith(clientName)).forEach(SIGN_PROCESS_INFO::remove);

            // 客户端掉线，发送邮件通知
            SignClientDto clientInfo = getSessionClientInfo(session);
            if (StringUtils.isNotBlank(status.getReason()) && status.getReason().contains(CommonConstant.CONNECT_TIMEOUT_REASON)
                    && clientInfo != null && clientInfo.getConfig() != null && StringUtils.isNotBlank(clientInfo.getConfig().getEmailAddr())) {
                emailUtils.sendClientOfflineMail(clientInfo.getConfig().getEmailAddr(), clientName);
            }
        }
    }

    /**
     * 消息处理
     *
     * @param session     会话
     * @param textMessage 消息
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) {
        String message = textMessage.getPayload();
        if (StringUtils.isBlank(message)) {
            log.warn("收到的消息为空");
            return;
        }
        WebSocketMessage msg;
        try {
            msg = JSON.parseObject(message, WebSocketMessage.class);
        } catch (Exception e) {
            log.warn("消息内容不合法！msg：{}", message);
            sendClientMessage(session, new WebSocketMessage()
                    .setType(WebSocketMessage.ERROR)
                    .setData("消息内容不合法!"));
            return;
        }
        String clientName = getSessionClientName(session);
        String msgType = msg.getType();
        // 心跳
        if (WebSocketMessage.HEARTBEAT.equals(msgType)) {
            // 发送心跳应答
            sendClientMessage(session, new WebSocketMessage().setType(WebSocketMessage.HEARTBEAT));
            // 微信小程序或网页，无需更新在线时间
            if (clientName.contains(CommonConstant.PAGE_CLIENT_NAME_CONTAINS)) {
                return;
            }

            LocalDateTime now = LocalDateTime.now();
            session.getAttributes().put("lastHeartbeat", LocalDateUtil.getTimestamp(now));
            // 保存最后在线时间
            if (!signClientService.updateOnlineTime(clientName, now)) {
                log.error("保存客户端最后在线时间失败!");
            }
            return;
        }

        String msgData = msg.getData();
        if (StringUtils.isBlank(msgData)) {
            log.warn("消息内容data为空！msg：{}", message);
            sendClientMessage(session, new WebSocketMessage()
                    .setType(WebSocketMessage.ERROR)
                    .setData("消息内容data为空!"));
            return;
        }
        switch (msgType) {
            case WebSocketMessage.SIGN_PROCESS: // 签到过程信息
                handleSignProcessMsg(msgData, clientName);
                break;
            case WebSocketMessage.SIGN: // 签到结果
                handleSignResultMsg(msgData, getSessionClientInfo(session));
                break;
            default:
                break;
        }
    }

    /**
     * 获取当前会话的客户端名称
     *
     * @param session 会话
     * @return String
     */
    private String getSessionClientName(WebSocketSession session) {
        return (String) session.getAttributes().get(CommonConstant.PARAM_CLIENT_NAME);
    }

    /**
     * 获取当前会话的客户端信息
     *
     * @param session 会话
     * @return SignClientDto
     */
    private SignClientDto getSessionClientInfo(WebSocketSession session) {
        return (SignClientDto) session.getAttributes().get(CommonConstant.PARAM_CLIENT_INFO);
    }

    /**
     * 处理签到过程消息
     *
     * @param msgData    消息
     * @param clientName 客户端名称
     */
    private void handleSignProcessMsg(String msgData, String clientName) {
        SignProcessMessage process;
        try {
            process = JSON.parseObject(msgData, SignProcessMessage.class);
            if (process.getSignRecordId() == null) {
                log.warn("[{} 客户端签到] 签到过程消息signRecordId为空！", clientName);
                return;
            }
        } catch (Exception e) {
            log.error("[{} 客户端签到] 解析签到过程消息异常！", clientName, e);
            return;
        }
        String signProcessKey = buildSignProcessInfoMapKey(clientName, process.getSignRecordId());
        List<SignProcessMessage> processList = SIGN_PROCESS_INFO.get(signProcessKey);
        if (CollectionUtils.isEmpty(processList)) {
            processList = new ArrayList<>();
        }
        processList.add(process);
        if (processList.size() > 1) {
            // 时间顺序排序
            processList = processList.stream()
                    .sorted(Comparator.comparing(SignProcessMessage::getDate))
                    .collect(Collectors.toList());
        }
        SIGN_PROCESS_INFO.put(signProcessKey, processList);

        // 实时推送签到过程给前端客户端（微信小程序或页面）
        String pageClientPrefix = buildPageClientNamePrefix(clientName, process.getSignRecordId());
        List<String> pageClientNameList = ONLINE_CLIENTS.keySet().stream().filter(key -> key.startsWith(pageClientPrefix)).collect(Collectors.toList());
        if (CollectionUtils.isEmpty(pageClientNameList)) {
            return;
        }
        for (String pageClientName : pageClientNameList) {
            WebSocketSession pageClientSession = ONLINE_CLIENTS.get(pageClientName);
            if (pageClientSession == null || !pageClientSession.isOpen()) {
                continue;
            }
            sendClientMessage(pageClientSession, new WebSocketMessage()
                    .setType(WebSocketMessage.SIGN_PROCESS)
                    .setData(JSON.toJSONString(processList)));
        }
    }

    /**
     * 处理签到结果消息
     *
     * @param msgData    消息
     * @param clientInfo 客户端信息
     */
    private void handleSignResultMsg(String msgData, SignClientDto clientInfo) {
        String clientName = clientInfo.getClientName();
        ScheduledFuture<?> scheduledFuture = SIGN_TIMEOUT_NOTIFY_LIST.remove(clientName);
        if (scheduledFuture != null) {
            scheduledFuture.cancel(true);
        }
        try {
            SignResultMessage result = JSON.parseObject(msgData, SignResultMessage.class);
            if (result.getStatusCode() == null) {
                result.setStatusCode(result.getSignTime() == null ? 0 : 1);
            }
            String emailAddr = clientInfo.getConfig().getEmailAddr();
            if (result.getStatusCode() == 1) {
                log.info("[{} 客户端签到] 签到成功.signTime：{}", clientName, result.getSignTime());
                // 签到成功邮件通知
                emailUtils.sendSignSuccessMail(emailAddr, result.getSignTime());
            } else {
                String msg = String.format("[%s 客户端签到] 签到失败！请查看签到记录详情", clientName);
                log.error(msg);
                emailUtils.sendSignErrorMail(emailAddr, msg);
            }
            SignRecordEntity signRecordEntity = new SignRecordEntity()
                    .setId(result.getSignRecordId())
                    .setSignTime(result.getSignTime());
            String signProcessKey = buildSignProcessInfoMapKey(clientName, result.getSignRecordId());
            List<SignProcessMessage> processMsgList = SIGN_PROCESS_INFO.get(signProcessKey);
            if (!CollectionUtils.isEmpty(processMsgList)) {
                // 时间顺序排序
                processMsgList = processMsgList.stream()
                        .sorted(Comparator.comparing(SignProcessMessage::getDate))
                        .collect(Collectors.toList());
                signRecordEntity.setDetailInfo(JSONArray.toJSONString(processMsgList));
                SIGN_PROCESS_INFO.remove(signProcessKey);
            }
            signRecordService.saveSignResult(signRecordEntity);
        } catch (Exception e) {
            log.error("[{} 客户端签到] 保存签到结果异常！", clientName, e);
        }
    }

    /**
     * 发送消息
     *
     * @param session 会话
     * @param msg     消息
     * @return boolean
     */
    private boolean sendClientMessage(WebSocketSession session, WebSocketMessage msg) {
        String msgJson = null;
        try {
            msgJson = JSON.toJSONString(msg);
            session.sendMessage(new TextMessage(msgJson));
        } catch (IOException e) {
            log.error("向 {} 客户端发送消息失败. msg：{}", getSessionClientName(session), msgJson, e);
            return false;
        }
        return true;
    }

    /**
     * 执行签到
     *
     * @param client 客户端配置
     */
    public Map<String, Object> executeSignIn(SignClientDto client) {
        String clientName = client.getClientName();
        if (SIGN_TIMEOUT_NOTIFY_LIST.containsKey(clientName)) {
            log.warn("[{} 客户端签到] 客户端正在签到中！", clientName);
            Map<String, Object> result = new HashMap<>(2);
            result.put("msg", "客户端正在签到中！");
            result.put("data", false);
            return result;
        }

        log.info("[{} 客户端签到] 开始执行签到", clientName);
        // 保存签到记录
        SignRecordEntity signRecordEntity = new SignRecordEntity()
                .setClientId(client.getId());
        signRecordService.save(signRecordEntity);
        Long signRecordId = signRecordEntity.getId();
        if (signRecordId == null) {
            log.error("[{} 客户端签到] 创建签到记录异常！clientId: {}", clientName, client.getId());
            Map<String, Object> result = new HashMap<>(2);
            result.put("msg", "创建签到记录异常！");
            result.put("data", false);
            return result;
        }
        log.info("[{} 客户端签到] 创建签到记录成功. {}", clientName, signRecordId);
        ClientConfigDto clientConfig = client.getConfig();
        // 发送签到指令
        Map<String, Object> map = sendSignCommand(client, signRecordId);
        // 返回签到记录ID
        map.put("signRecordId", signRecordId);
        if ((boolean) map.get("data")) {
            // 创建签到超时提醒任务
            createSignTimeoutTipTask(clientConfig, clientName, signRecordId);
        } else {
            // 任务执行异常发送邮件通知
            emailUtils.sendSignErrorMail(clientConfig.getEmailAddr(), (String) map.get("msg"));
        }
        return map;
    }

    /**
     * 创建签到超时提醒任务
     *
     * @param clientConfig 客户端配置
     * @param clientName   客户端名称
     * @param signRecordId 签到记录ID
     */
    private void createSignTimeoutTipTask(ClientConfigDto clientConfig, String clientName, Long signRecordId) {
        // 若x分钟后未收到客户端签到结果消息，则发送邮件通知
        int minute = Math.max(clientConfig.getEstimateExecMin(), 2);
        SIGN_TIMEOUT_NOTIFY_LIST.put(clientName, taskScheduler.schedule(() -> {
            SignRecordEntity signRecord = signRecordService.getById(signRecordId);
            if (signRecord != null && signRecord.getStatusCode() == null) {
                String msg = String.format("[%s 客户端签到] 发送签到指令%d分钟后，未收到客户端签到结果消息！", clientName, minute);
                log.error(msg);
                emailUtils.sendSignErrorMail(clientConfig.getEmailAddr(), msg);
            }
            SIGN_TIMEOUT_NOTIFY_LIST.remove(clientName);
        }, Instant.now().plus(Duration.ofMinutes(minute))));
    }

    /**
     * 向指定客户端发送签到指令
     *
     * @param client       客户端信息
     * @param signRecordId 签到记录ID
     */
    public Map<String, Object> sendSignCommand(SignClientDto client, Long signRecordId) {
        String clientName = client.getClientName();
        WebSocketSession session = ONLINE_CLIENTS.get(clientName);
        if (session == null || !session.isOpen()) {
            log.error("[{} 客户端签到] 客户端不在线，无法执行！", clientName);
            Map<String, Object> result = new HashMap<>(2);
            result.put("msg", "客户端不在线，无法执行！");
            result.put("data", false);
            return result;
        }
        ClientConfigDto clientConfig = client.getConfig();
        ExecSignMessage execSignMsg = new ExecSignMessage();
        BeanUtils.copyProperties(clientConfig, execSignMsg);
        execSignMsg.setSignRecordId(signRecordId);
        WebSocketMessage webSocketMessage = new WebSocketMessage()
                .setType("sign")
                .setData(JSON.toJSONString(execSignMsg));
        if (!sendClientMessage(session, webSocketMessage)) {
            log.error("[{} 客户端签到] 发送签到指令失败！", clientName);
            Map<String, Object> result = new HashMap<>(2);
            result.put("msg", "发送签到指令失败！");
            result.put("data", false);
            return result;
        }

        log.info("[{} 客户端签到] 发送签到指令成功", clientName);
        Map<String, Object> result = new HashMap<>(2);
        result.put("msg", "发送签到指令成功");
        result.put("data", true);
        return result;
    }

    /**
     * 根据客户端名称关闭客户端连接
     *
     * @param clientName 客户端名称
     * @param reason     关闭原因
     * @return boolean
     */
    public boolean removeClientWebsocket(String clientName, String reason) {
        if (StringUtils.isBlank(clientName)) {
            log.error("客户端名称为空");
            return false;
        }
        WebSocketSession session = ONLINE_CLIENTS.get(clientName);
        if (session == null) {
            return true;
        }
        try {
            session.close(new CloseStatus(1000, reason));
            Thread.sleep(500);
        } catch (IOException e) {
            log.error("关闭 {} 客户端连接异常", clientName, e);
        } catch (InterruptedException e) {
            log.error("线程中断", e);
        }
        return !session.isOpen();
    }

    /**
     * 清空签到过程集合，防止异常情况出现脏数据堆积
     */
    public void clearSignProcessInfo() {
        if (SIGN_PROCESS_INFO.isEmpty()) {
            return;
        }
        SIGN_PROCESS_INFO.clear();
    }

    /**
     * 构建前端过程集合的 key
     *
     * @param clientName   客户端名称
     * @param signRecordId 签到记录ID
     * @return String
     */
    private String buildSignProcessInfoMapKey(String clientName, Long signRecordId) {
        return clientName + "-" + signRecordId;
    }

    /**
     * 构建前端客户端名称（微信小程序或页面）
     *
     * @param clientName   客户端名称
     * @param signRecordId 签到记录ID
     * @return String clientName-page-signRecordId-wx|web-uuid
     */
    private String buildPageClientNamePrefix(String clientName, Long signRecordId) {
        return clientName + CommonConstant.PAGE_CLIENT_NAME_CONTAINS + signRecordId + "-";
    }

}